fix(storage): reject path-traversal artifact ids at the path chokepoint#301
fix(storage): reject path-traversal artifact ids at the path chokepoint#301minion1227 wants to merge 6 commits into
Conversation
`Source.id` is locked to a hex sha256, but `Claim`, `Page`, `Entity`, `Relation`, `Evidence`, `Session`, and `Proposal` all carry a bare `id: str` with no validator. `_yaml` / `_page_path` turned those ids straight into filesystem paths with no containment check, so a poisoned id — e.g. `"../../tmp/x"` smuggled through a bundle body that `import_apply` lands under a safe filename but whose in-memory id is traversal — could escape `kb_dir` on the next put / update / lifecycle write, or read an arbitrary file on get. add `_reject_unsafe_id`, mirroring `bundle._unsafe_name_reason`: an id must be non-empty and free of nul bytes, path separators, and `..` components. route it through `_yaml` and `_page_path` — the single chokepoint every `_*_path` helper funnels through — so all artifact read/write paths are guarded at once. a complementary model-layer id validator (mirroring `Source._id_is_hex_sha256`) is left as a follow-up; the storage guard already closes every reach path, including raw-id reads a model validator cannot see. closes vouchdev#149
WalkthroughAdds centralized storage-layer validation for unsafe artifact IDs, applies it to YAML, page, and source path construction, expands regression coverage for rejected and valid IDs, and documents the hardening in the changelog. ChangesArtifact ID Path Traversal Guard
Estimated code review effort: 2 (Simple) | ~12 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/vouch/storage.py`:
- Around line 257-265: The storage path validation is incomplete because source
paths are still built from untrusted source IDs without rejection. Update the
source lookup flow in Storage, especially get_source(), read_source_content(),
and _source_dir(), to apply _reject_unsafe_id() to source_id before constructing
paths so traversal inputs cannot escape kb_dir. Keep the fix aligned with the
existing _yaml(), _page_path(), and _claim_path() validation pattern.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 868a3b3e-60b5-42f6-bee6-d17f55afff2f
📒 Files selected for processing (3)
CHANGELOG.mdsrc/vouch/storage.pytests/test_storage.py
address review on vouchdev#301: `_yaml` / `_page_path` were guarded, but `get_source` / `read_source_content` (and the evidence-ref existence checks) route raw `source_id` strings through `_source_dir` without it, so `get_source("../../outside")` could still escape kb_dir to `<outside>/meta.yaml` / `content`. `Source.id` is hex-locked on the model but these read paths take unvalidated strings. apply `_reject_unsafe_id` at the `_source_dir` chokepoint too, closing the last storage traversal path. add get_source / read_source_content rejection tests.
|
good catch — fixed in a5db33c. added |
|
@copilot resolve the merge conflicts in this pull request |
resolves the storage.py conflict from vouchdev#301 after main advanced to 1.2.1. - storage.py: keep both additions at the same spot — the vouchdev#149 `_reject_unsafe_id` path-traversal guard (this branch) and main's `_log` / `_load_or_skip` corrupt-file resilience helpers. the guard's three call sites in the `_*_path` helpers survived the merge intact. - changelog: move the vouchdev#149 entry into `[Unreleased]`; main had already cut the section it originally sat under into [1.0.0].
resolves the vouchdev#301 conflicts against its actual base branch (test, not main) after test advanced to vouchdev#345. - storage.py: keep both additions at the same spot — the vouchdev#149 `_reject_unsafe_id` path-traversal guard (this branch) and test's `_log` / `_load_or_skip` corrupt-file resilience helpers. the guard's three call sites in the `_*_path` helpers survived intact. - changelog / test_storage.py: keep both this branch's vouchdev#149 entries and the sibling vouchdev#155 empty-content entries test already carried.
…package the vouchdev#237 host-compat drift check read openclaw.compat.pluginApi from openclaw.plugin.json, but the 2026.6 loader repackage (47ac72f) moved that floor into the new loader-facing package.json — the current openclaw parser ignores openclaw.plugin.json's openclaw.* fields entirely. so _load_host_compat() read a key that no longer existed, host_compat silently degraded to {}, and the two drift tests failed on test. point _load_host_compat() and the drift tests at package.json, where the pluginApi floor now lives. the graceful-degradation and malformed-json tests move with it. no behaviour change beyond reading the right file.
# Conflicts: # CHANGELOG.md # src/vouch/capabilities.py # tests/test_capabilities.py
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Around line 378-379: Move both fix entries currently under the 1.0.0 section
into the existing [Unreleased] ### Fixed section of CHANGELOG.md, preserving
their wording and ordering relative to the other unreleased fixes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 68c4d998-bb6d-41c9-b83f-556253679a66
📒 Files selected for processing (2)
CHANGELOG.mdsrc/vouch/storage.py
🚧 Files skipped from review as they are similar to previous changes (1)
- src/vouch/storage.py
| - artifact ids that contain a path separator, `..`, a nul byte, or are empty are now rejected at the storage layer, closing a path-traversal hole. `Source.id` is hex-locked, but the other models (`Claim`, `Page`, `Entity`, `Relation`, `Evidence`, `Session`, `Proposal`) carry a bare `id: str`, so a poisoned id — e.g. one smuggled through a bundle body under a safe on-disk filename — could resolve to a path outside `kb_dir` on the next put / update / lifecycle write (or read an arbitrary file on get). The guard sits at the `_yaml` / `_page_path` chokepoint every `_*_path` helper funnels through, mirroring the `bundle._safe_member_path` tar-traversal fix (fixes #149). | ||
| - `Claim.text`, `Entity.name`, and `Page.title` now reject empty / whitespace-only values at the model layer via `@field_validator`s, mirroring the `Claim.evidence` min-citation validator (#81/#82). The non-empty contract previously lived only in the `propose_*` helpers, so `store.put_*`, `store.update_*`, and bundle/sync import all silently accepted blank-content artifacts; enforcing on the model closes every write path at once (fixes #155). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Move these entries under [Unreleased].
Both new fixes are currently listed under ## [1.0.0], so they will be attributed to an already released version instead of appearing in the next release notes. Move Lines 378-379 into the [Unreleased] ### Fixed section.
As per coding guidelines, user-visible features must be documented under [Unreleased] in the same PR.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CHANGELOG.md` around lines 378 - 379, Move both fix entries currently under
the 1.0.0 section into the existing [Unreleased] ### Fixed section of
CHANGELOG.md, preserving their wording and ordering relative to the other
unreleased fixes.
Source: Coding guidelines
What changed
Artifact ids are now rejected at the storage layer when they contain a path
separator,
.., a nul byte, or are empty. A new_reject_unsafe_idguardruns at the
_yaml/_page_pathchokepoint that every_*_pathhelperfunnels through, so all artifact read/write paths are covered at once.
Why
Source.idis locked to a hex sha256, butClaim,Page,Entity,Relation,Evidence,Session, andProposalall declare a bareid: strwith no validator, and_yamlturned that straight intokb_dir/<sub>/<id>.yamlwith no containment check. A poisoned id could thenescape
kb_dir:import_applyuses_safe_member_pathto keep each tar member's filename insidekb_dir,but does not check the
idfield inside the YAML body. A claim landedunder
claims/innocent.yamlwhoseidis"../../../tmp/pwned"readsback fine (safe filename), but the next
update_claim/supersede/contradictresolves_claim_path(claim.id)to a path outsidekb_dir.get_claim("../../../etc/hostname")resolvedstraight through
_yamlbefore this change.This is the same class of hole the tar-member fix (
bundle._safe_member_path,the CVE-2007-4559 fix) already closed on the import path —
_reject_unsafe_idmirrors its
_unsafe_name_reasonchecks at the storage chokepoint. closes #149.What might break
nothing legitimate. artifact ids are flat slugs / hex / uuids — none contain
/,\,.., or nul — so every real read/write is unaffected (the fullsuite, including bundle/sync/session round-trips, is green). the only calls
that now raise
ValueErrorare ones that were already unsafe. no on-diskformat, kb.* method, or audit-log shape changes.
a complementary model-layer id validator (mirroring
Source._id_is_hex_sha256) is deliberately left as a follow-up: the storageguard already closes every reach path, including raw-id reads a model
validator cannot see, and keeping it storage-only avoids overlapping the
model classes.
VEP
not a surface change — a storage-layer containment guard in the same shape
already merged for tar members. no VEP needed.
Tests
make check— ruff clean; mypy clean on touched files; pytest green(only pre-existing, environment-only failures remain:
fastapi/[web]not installed and embeddings installed, both of which also fail on
main)tests/test_storage.py:_yaml/_page_pathrejecttraversal / separator / absolute / empty ids (parametrised);
put_*,get_*, andput_pagereject traversal ids; the disk-smuggled-id →update_claimexploit chain is blocked before any write; and aregression guard that legitimate slug / hex ids still resolve
CHANGELOG.mdupdated under## [Unreleased]Summary by CodeRabbit